# -*- coding: utf-8 -*-

#   This file is part of emesene.
#
#    Emesene is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    emesene is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with emesene; if not, write to the Free Software
#    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

''' DnDRoller Plugin

This plugin allows the user to roll DnD-style dice.

The usage syntax is /roll followed by a list of dice rolls in XdY format
(X: number of dice, Y: numer of sides per die) and bonus modifiers separated
by "+" signs. For example:
    /roll 2d8+1d12+10

Version History:

Version 0.1 -- 21 Mar 2009
-Initial release.

Version 0.2 -- 24 Mar 2009
-Now any of your contacts can use this feature, regardless of the MSN client
 they're using. The plugin will parse any incoming message beginning with
 /roll and automatically send back the result through the chat window.

Version 0.3 -- 26 Mar 2009
-Solved a bug where spaces in a nickname would be sent as a '%20' character.
-Limited number and type of dice to 1000.
-Added several code snippets to handle incorrect input (sending an error
 message to the user invoking it).
-Refactored some code.

Implementation details:

The plugin registers the /roll command withn the class SlashCommands, and the
class method ownRoll is called when the command is invoked by the user. The
result is delivered by the slash_action.OutputText method. On the other hand,
in order to allow external users to use the command, the plugins also connects
to the switchboard, waiting for the "message" signal and specifying own method
buddyRoll as the callback. In this case, the result is delivered directly
through the conversation.sendMessage method.
'''

VERSION = '0.3'
from Plugin import Plugin
from random import randint

class MainClass(Plugin):

    description = _('Rolls DnD-style dice. Usage: /roll followed by a list'
    ' of valid DnD dice rolls (such as 2d6) and bonus modifiers, separated by'
    ' "+" signs. Example: /roll 2d6+1d10+5.')
    authors = { 'Meithan West' : 'meithan@hotmail.com' }
    website = 'None'
    displayName = _('DnD Dice Roller')
    name = 'DnDRoller'

    def __init__(self, controller, msn):
        Plugin.__init__(self, controller, msn)

        self.description = _('Rolls DnD-style dice. Usage: /roll followed by'
        ' a list of valid DnD dice rolls (such as 2d6) and bonus modifiers, '
        'separated by "+" signs. Example: /roll 2d6+1d10+5.')
        self.authors = { 'Meithan West' : 'meithan@hotmail.com' }
        self.website = 'None'
        self.displayName = _('DnD Dice Roller')
        self.name = 'DnDRoller'

        self.handlerID = None  # handler for signal connection
        self.action = None     # local alias for slashAction
        self.controller = controller
        self.Slash = controller.Slash

        self.enabled = False

    def start(self):
        '''Plugin start'''
        # register command with SlashCommands for own rolls
        self.Slash.register('roll', self.ownRoll, _('DnD-style dice roller.'
        ' Usage: \nString together rolls, in NdT format, and bonuses, separated'
        ' by + signs.\nExample: "2d6+1d10+5"'))
        # connect to the switchboard::message signal to catch incoming messages
        self.handlerID = self.connect('switchboard::message',self.buddyRoll)
        self.enabled = True

    def ownRoll(self, slashAction):
        '''Invoke roll from SlashCommands'''
        # Get params from SlashAction
        self.action = slashAction
        data = self.action.getParams()
        nick = self.controller.msn.nick
        # Get result and build answer
        result = self.parseRoll(data)
        if (result=='invalid'):
            answer = 'Correct roll format is XdY+B, where X: number ' \
                     'of dice, Y: type of dice, B: numeric bonus'
            send = False
        elif (result=='overflow'):
            answer = 'Too many dice, or die type too big'
            send = False
        else:
            answer = nick + result
            send = True
        self.action.outputText(answer, send)

    def buddyRoll(self, msn, switchboard, signal, args):
        '''Looks for /roll command in received messages'''
        # separate signal arguments
        mail = args[0]
        nick = args[1]
        # Get rid of spaces in nickname
        if (nick.count('%20')!=0):
            nick = nick.replace('%20',' ')
        message = args[2]
        # get conversation handle
        conversation = self.controller.conversationManager.\
                       getOpenConversation(mail,switchboard)[1]
        # Detect /roll command in received message
        if (message.startswith('/roll')):
            data = message.split(' ',1)
            if (len(data)>=2):
                data = data[1]
            # Generate rolls and build answer
            result = self.parseRoll(data)
            if (result=='invalid'):
                answer = 'Correct roll format is XdY+B, where X: number'\
                         ' of dice, Y: type of dice, B: numeric bonus'
            elif (result=='overflow'):
                answer = 'Too many dice, or die type too big'
            else:
                answer = nick + result
            conversation.sendMessage(answer)

    def parseRoll(self, data):
        '''Parses command arguments, calls roll() and returns the results'''
        # Separate arguments
        numargs = data.count('+')+1
        if (numargs==1):
          args = [data]
        else:
          args = data.split('+')
        # Initialize
        bonus = 0
        rolls = []
        for arg in args:
        # If it's dice, roll 'em.
        # If it's a number, just add to bonus.
            if (arg.count('d')==1):
                rolls += self.doRoll(arg)
                # Check if result is valid
                if (rolls[-1]==0):
                    return 'invalid'
                if (rolls[-1]==-1):
                    return 'overflow'
            if (arg.count('d')==0):
                try:
                    bonus += int(arg)
                except ValueError:
                    return 'invalid'
        # Calculate total
        total = reduce(lambda x,y: x+y, rolls, 0) + bonus
        # Generate result string
        if (numargs==1 and data.startswith('1d')):
            result = ' rolls ' + data + ': ' + str(rolls)
        elif (bonus==0):
            result = ' rolls ' + data + ': ' + str(rolls) + ' = ' + str(total)
        else:
            result = ' rolls ' + data + ': ' + str(rolls) +  '+' + \
                     str(bonus) + ' = ' + str(total)
        # Send result
        return result

    def doRoll(self, dice):
        '''Rolls N dice of type T, returns list of rolls'''
        params = dice.split('d')
        # Get N and T
        try:
            N = int(params[0])
            T = int(params[1])
        except ValueError:
            return [0]
        # Prevent trolling
        if (N>1000):
            return [-1]
        if (T>1000):
            return [-1]
        # If all is good, do the rolls
        rolls = []
        for i in range(N):
            rolls.append(randint(1, T))
        return rolls

    def stop(self):
        self.controller.msn.disconnect(self.handlerID)
        self.Slash.unregister('roll')
        self.enabled = False

    def check(self):
        return (True, 'Ok')
